package risoe.syslab.fhnew.gui;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;
import org.apache.batik.bridge.ViewBox;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.svg.SVGLoadEventDispatcherAdapter;
import org.apache.batik.swing.svg.SVGLoadEventDispatcherEvent;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGPreserveAspectRatio;
import org.w3c.dom.svg.SVGRect;
import org.w3c.dom.svg.SVGSVGElement;

/**
 * Class responsible for showing Batik SVG renderings.
 * @author M. Svendsen
 */
public class Visualization extends JPanel{
	private static final long serialVersionUID = -4199426308850406377L;

	private String mName;
	private JSVGCanvas mCanvas;
	private SVGDocument mDocument;
	private UIViewer mViewer;
	private String[] mSubscribedObjects;
	private boolean mLoaded = false;

	/**
	 * Constructs a new Visualization
	 * @param name a string representing the name of this visualization. Should match the name of any related controller
	 * @param subscribedObjects an array of IDs that should be listened to
	 * @param viewer the UIViewer that is to show this Visualization
	 */
	public Visualization(String name, String[] subscribedObjects, UIViewer viewer) {
		mName = name;
		mViewer = viewer;
		mSubscribedObjects = subscribedObjects;

		// Create a new layout and canvas and add the created canvas to the layout
		setLayout(new BorderLayout());

	}

	/**
	 * Loads a document into this Visualization and begins rendering. This function should be called when
	 * the user navigates to this Visualization
	 * @param document the SVGDocument to load
	 */
	public void load(SVGDocument document){
		if (!mLoaded){
			mCanvas = new ScaleableJSVGCanvas();

			add(mCanvas);

			// Forces the canvas to always be dynamic even if the current
			// document does not contain scripting or animation.
			mCanvas.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC);

			// The code in this event handler will be called after the SVG file is fully loaded.
			mCanvas.addSVGLoadEventDispatcherListener(new SVGLoadEventDispatcherAdapter() { 
				public void svgLoadEventDispatchStarted
				(SVGLoadEventDispatcherEvent e) {
					// The document is now available. Recalculate size and call subclass
					mDocument = mCanvas.getSVGDocument();

					// Invalidate display
					mCanvas.revalidate();

					// Check if controller has subscribed to any events
					if (mSubscribedObjects.length > 0) {
						for (String id : mSubscribedObjects) {
							Element element = mDocument.getElementById(id);
							if (element != null) {
								new MouseEventHandler(mName, id, (EventTarget) element, Visualization.this);
							}
							else {
								System.out.println("No element with id: " + id);
							}
						}
					}		
					// Fallback to reporting all events
					else {
						mCanvas.addMouseMotionListener(new MouseMotionListener() {

							@Override
							public void mouseMoved(MouseEvent e) {
								// This event is irrelevant for use with a touchscreen
							}

							@Override
							public void mouseDragged(MouseEvent e) {
								updatePosition(e);
								issueEvent(new VisualizationEvent(e, getName()));
							}
						});

						mCanvas.addMouseListener(new MouseListener() {

							@Override
							public void mouseReleased(MouseEvent e) {
								updatePosition(e);
								issueEvent(new VisualizationEvent(e, getName()));
							}

							@Override
							public void mousePressed(MouseEvent e) {
								updatePosition(e);
								issueEvent(new VisualizationEvent(e, getName()));
							}

							@Override
							public void mouseExited(MouseEvent e) {
								// This event is irrelevant for use with a touchscreen
							}

							@Override
							public void mouseEntered(MouseEvent e) {
								// This event is irrelevant for use with a touchscreen
							}

							@Override
							public void mouseClicked(MouseEvent e) {
								updatePosition(e);
								issueEvent(new VisualizationEvent(e, getName()));
							}
						});
					}
				}
			});

			mCanvas.setDocument(document);

			mLoaded = true;
		}
	}

	/**
	 * Unloads a previously loaded document. This function should be called when the user navigates
	 * away from this Visualization.
	 */
	public void unload(){
		if (mLoaded){
			remove(mCanvas);
			shutdown();
		}

		mLoaded = false;
	}

	/**
	 * @return the JSVGCanvas that this visualization draws into
	 */
	protected JSVGCanvas getCanvas(){
		return mCanvas;
	}

	/**
	 * @return the Document describing all SVG XML elements
	 */
	protected Document getDocument(){
		return mDocument;
	}

	@Override
	public String getName() {
		return mName;
	}

	/**
	 * Translates a MouseEvent to coincide with the actual(unscaled) location.
	 * This method is used when the entire Visualization has been scaled by a smaller/larger screen
	 * @param e the MouseEvent to rescale
	 */
	private void updatePosition(MouseEvent e){
		ScaleableJSVGCanvas canvas = (ScaleableJSVGCanvas)mCanvas;
		e.translatePoint((int)(e.getX()/canvas.mScaleX-e.getX()), (int)(e.getY()/canvas.mScaleY-e.getY()));
	}

	/**
	 * Translates a Point obtained through a mouse related event to the actual location.
	 * This method is used when the entire Visualization has been scaled by a smaller/larger screen
	 * @param p the point to scale
	 */
	public void updatePosition(Point p){
		ScaleableJSVGCanvas canvas = (ScaleableJSVGCanvas)mCanvas;
		p.move((int)(p.getX()/canvas.mScaleX), (int)(p.getY()/canvas.mScaleY));
	}


	/**
	 * Sends a visualizationevent to any registered listeners
	 * @param event the event to send
	 */
	public final void issueEvent(VisualizationEvent event){
		// Call viewer
		mViewer.onVisualizationEvent(event);
	}

	/**
	 * Updates the current Visualization. Decides whether this update should be committed right away
	 * or scheduled on the UI thread(when this visualization is visible)
	 * @param command the VisualizationCommand to apply to this visualization
	 */
	public void executeVisualizationCommand(final VisualizationUpdate command){
		// Check whether this visualization is currently visible
		if (mLoaded && mCanvas.getUpdateManager() != null){
			mCanvas.getUpdateManager().getUpdateRunnableQueue().invokeLater( new Runnable() {

				@Override
				public void run() {
					DocumentUtils.updateDocument(command, mDocument);
				}
			});
		}
	}

	/**
	 * Stops all activity related to this Visualization. Should be called before unloading/removing the Visualization.
	 */
	private void shutdown(){
		if (mCanvas != null){
			mCanvas.stopProcessing();
			mCanvas.dispose();
			mCanvas.setURI(null);
			mCanvas = null;
		}
	}

	/**
	 * Carries out the scaling of SVG files in order to fit the size of the target screen.
	 */
	@SuppressWarnings("serial")
  private static class ScaleableJSVGCanvas extends JSVGCanvas{
		public double mScaleX = 1.0;
		public double mScaleY = 1.0;

		@Override
		protected AffineTransform calculateViewingTransform(String fragIdent, SVGSVGElement svgElt) {
			// Get available size
			Dimension d = getSize();
			if (d.width  < 1) d.width  = 1;
			if (d.height < 1) d.height = 1;

			// Define the transformation based on presence of an viewbox element in the svg file
			AffineTransform tf;
			String viewBox= svgElt.getAttributeNS(null, ViewBox.SVG_VIEW_BOX_ATTRIBUTE);
			if (viewBox.length() == 0) {
				// No viewbox specified, create a custom viewbox
				float[] vb= new float[4];
				SVGRect rc= svgElt.getBBox();
				vb[0]= rc.getX();
				vb[1]= rc.getY();
				vb[2]= rc.getWidth();
				vb[3]= rc.getHeight();
				tf= ViewBox.getPreserveAspectRatioTransform(vb,
						SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID, true, d.width, d.height);
			} else {
				// Apply the user-specified viewbox
				String aspectRatio= svgElt.getAttributeNS(null, ViewBox.SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE);
				if (aspectRatio.length() > 0)
					tf= ViewBox.getPreserveAspectRatioTransform(svgElt, viewBox, aspectRatio, d.width, d.height, bridgeContext);
				else {
					float[] vb= ViewBox.parseViewBoxAttribute(svgElt, viewBox, bridgeContext);
					tf= ViewBox.getPreserveAspectRatioTransform(
							vb, SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMAX, true, d.width, d.height);
				}
			}

			// Remember scaling
			mScaleX = tf.getScaleX();
			mScaleY = tf.getScaleY();

			return tf;
		}
	}
}
